﻿/*  This file is part of TextEditorLibrary - a library for the programmatic manipulation of text.

    TextEditorLibrary is free software: you can redistribute it and/or modify it under the terms
    of the GNU Lesser General Public License as published by the Free Software Foundation, either
    version 3 of the License, or (at your option) any later version.

    TextEditorLibrary is distributed in the hope that it will be useful, but WITHOUT ANY 
    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
    PURPOSE.  See the GNU Lesser General Public License for more details at 
    <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace TextEditorLibrary
{

/// <summary>Defines a span between two Marks in a Text which move with the text.</summary>
    public partial class Range
    {
        private static int _IDs = 0;   // Number of registered Ranges so far
        private static object _idLock = new object(); // Locking object to protect _IDs
        internal int _ID;               // Range ID for debug

        internal Mark _mark1;           // Start mark
        internal Mark _mark2;           // End mark

    // Can't construct from the API - use Text.AddRange(); we assume the Marks have been created
    // privately before the Range is constructed
        internal Range(Mark mark1, Mark mark2)
        {
            _ID = 0;
            _mark1 = mark1;
            _mark2 = mark2;
        }

        internal static int GetNextID()
        {
            lock (_idLock)
                return ++_IDs;
        }

    //---------------------------------------------------------------------------------------------

    // If a Range is unregistered its Marks can become invalid when the text changes, so check.

        internal void Validate(string argument)
        {
            if (_mark1._ID < 0 || _mark2._ID < 0)
                throw new ArgumentException("Range argument is invalid.", argument);
        }

        internal void ValidateThis()
        {
            if (_mark1._ID < 0 || _mark2._ID < 0)
                throw new InvalidOperationException("This Range is invalid.");
        }

    // We often need the Range to be in a specific text as well.

        internal void Validate(Text text, string argument)
        {
            if (_mark1._ID < 0 || _mark2._ID < 0)
                throw new ArgumentException("Range argument is invalid.", argument);
            if (_mark1._text != text)
               throw new ArgumentException("Range argument is in a different Text.", argument);
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary><para>Moves the Range to the span of a given Range value.</para>
    /// <para>The Range is returned to allow chaining of calls.</para></summary>
    /// <param name="range">Range value defining the new span.</param>
    /// <exception cref="ArgumentException">The passed Range has been invalidated.</exception>
        public Range MoveTo(Range range)
        {
            range.Validate(_mark1._text, "range");

            _mark1._block = range._mark1._block;
            _mark1._position = range._mark1._position;
            _mark2._block = range._mark2._block;
            _mark2._position = range._mark2._position;

        // If the Range was invalidated, then it becomes invalidatable again - usable until text
        // changes
            if (_mark1._ID<0) _mark1._text.Invalidatable(_mark1);
            if (_mark2._ID<0) _mark1._text.Invalidatable(_mark2);

           _mark1._text.Check();
            return this;
        }

    /// <summary><para>Moves the Range to lie between the given Marks.</para>
    /// <para>The Range is returned to allow chaining of calls.</para></summary>
    /// <param name="mark1">Mark value defining the start of the new span.</param>
    /// <param name="mark2">Mark value defining the end of the new span.</param>
    /// <exception cref="ArgumentException">At least one of the passed Marks has been invalidated,
    /// or is in a different Text.</exception>
        public Range MoveTo(Mark mark1, Mark mark2)
        {
            mark1.Validate(_mark1.Text, "mark1");
            mark2.Validate(mark1._text, "mark2");
            if (mark2 - mark1 < 0)
                throw new ArgumentException("Mark arguments are in the wrong order.",
                                                                                  "mark1, mark2");
            
            _mark1._block = mark1._block;
            _mark1._position = mark1._position;
            _mark2._block = mark2._block;
            _mark2._position = mark2._position;

        // If the Range was invalidated, then it becomes invalidatable again - usable until text
        // changes
            if (_mark1._ID<0) _mark1._text.Invalidatable(_mark1);
            if (_mark2._ID<0) _mark1._text.Invalidatable(_mark2);

            _mark1._text.Check();
            return this;
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary><para>Deletes the text in the Range.</para>
    /// <para>All registered Marks in the Range move to the start (and end) of the Range. The Range 
    /// is returned to allow chaining of calls.</para></summary>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public Range Delete()
        {
            DoDelete();
            _mark1._text.InvalidateAll();

        // We know that this Range's Marks are still valid.
            if (_mark1._ID < 0) _mark1._text.Invalidatable(_mark1);
            if (_mark2._ID < 0) _mark1._text.Invalidatable(_mark2);
            return this;
        }

    /// <summary><para>Replaces the text in the Range with a given string.</para>
    /// <para>All registered Marks initially in the Range end up at the start of the Range. The 
    /// Range is returned to allow chaining of calls.</para></summary>
    /// <param name="text">string value containing the text for insertion.</param>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public Range Replace(string text)
        {
            List<Mark> endMarks = DoDelete();
            _mark2.Insert(text);

        // DoDelete saved a list of registered marks which were at the end of the Range.
            foreach (Mark mark in endMarks)
                mark.MoveTo(_mark2);

        // We know that this Range's Marks are still valid.
            if (_mark1._ID < 0) _mark1._text.Invalidatable(_mark1);
            if (_mark2._ID < 0) _mark1._text.Invalidatable(_mark2);
            return this;
        }

    /// <summary><para>Replaces the text in the Range.</para>
    /// <para>All registered Marks initially in the Range end up at the start of the Range. The 
    /// Range is returned to allow chaining of calls.</para></summary>
    /// <param name="source">object whose ToString method will define the text for insertion.</param>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public Range Replace(object source)
        {
            return Replace(source.ToString());
        }

    /// <summary><para>Deletes the text in the Range, returning the original contents.</para>
    /// <para>All registered Marks in the Range move to the start (and end) of the Range.</para></summary>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public string Cut()
        {
            string contents = ToString();
            DoDelete();
            _mark1._text.InvalidateAll();
            return contents;
        }

    // Do the actual delete, returning a list of Marks which were at the end of the Range, in case
    // this is the first part of a Replace
        internal List<Mark> DoDelete()
        {
            ValidateThis();

        // Unregistered copy of end Mark so that it will not get moved by Pull if registered
            Mark mark2 = new Mark(_mark2, false);

        // Move all Marks inside the Range back to the start, but remember which ones were at
        // the end in case this is part of a Replace
            Text text = _mark1._text;
            List<Mark> endMarks = text.Pull(_mark1, mark2);

        // Split the last block at the end of the Range - Mark stays with last bit to be deleted
            text.Split(mark2._block, mark2._position);

        // If the Range lies in one block, then just truncate the block; we've just loaded the block
        // and marked it changed
            if (mark2._block == _mark1._block)
            {
                text._editLength -= _mark1._block._data.Length - _mark1._position;
                _mark1._block._data.Length = _mark1._position;
            }

        // Otherwise we are safe to split the first block at the start of the Range - Mark stays
        // with the last block to keep
            else
            {
                text.Split(_mark1._block, _mark1._position);

            // Whip out the blocks in between - no need for GetNext because the previous Split
            // has always created a next block
                TextBlock blockAfter = mark2._block._next;
                TextBlock block = _mark1._block._next;
                do
                {
                    text._editLength -= block.GetLength();
                    text._blocks.Remove(block);
                    block = _mark1._block._next;
                } while (block != blockAfter);
            }

        // Move the last mark to the first
            _mark2._block = _mark1._block;
            _mark2._position = _mark1._position;

        // See if we can stick the bits back together - twice in case we removed whole blocks
            text.Join(_mark1._block);
            text.Join(_mark1._block);

            text.Check();

        // See if we have made any Boxes dirty - assume not many, all short
            foreach (Box box in text._boxes)
            {
                if (!box.DirtyRange && box.TextLength != box.Range.Length()) box.OnRangeDirty();
            }

            return endMarks;
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary>Converts the Range contents to a string with newline characters at line ends.
    /// </summary>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public override string ToString()
        {
            ValidateThis();

            StringBuilder output = new StringBuilder();

            TextBlock block = _mark1._block;
            int position = _mark1._position;
            int copy;

            StringBuilder sb = block.GetData();

        // First block and up to but not including last block
            while (block != _mark2._block)
            {
                sb = block.GetData();
                copy = sb.Length - position;
                output.Append(sb.ToString(position,copy));
                position = 0;
                block = block._next;
            }

        // Last block
            sb = block.GetData();
            copy = _mark2._position - position;
            output.Append(sb.ToString(position,copy));

            return output.ToString();
        }

    //---------------------------------------------------------------------------------------------

    /// <summary>Returns the length of the Range, including newline characters.</summary>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public long Length()
        {
            ValidateThis();

            long length = 0;
            int start = _mark1._position;
            TextBlock block = _mark1._block;

            while (block != _mark2._block)
            {
                length += block.GetLength() - start;
                start = 0;
                block = block._next;
            }
            length += _mark2._position - start;
            return length;
        }

    /// <summary>Returns true if the passed Range has the same extent as this Range.</summary>
    /// <param name="range">Range to compare against.</param>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
    /// <exception cref="ArgumentException">The passed Range has been invalidated, or is in a
    /// different Text.</exception>
        public bool SameAs(Range range)
        {
            ValidateThis();
            range.Validate(_mark1._text, "range");

            return (range._mark1._block==_mark1._block && range._mark1._position==_mark1._position &&
                    range._mark2._block==_mark2._block && range._mark2._position==_mark2._position);
        }

    /// <summary><para>Removes the Range from the list of Ranges registered with its Text.</para>
    /// <para>The Range will be invalidated by subsequent changes to the text.</para></summary>
        public void Remove()
        {
            _mark1._text.Remove(this);
        }

    //---------------------------------------------------------------------------------------------

    /// <summary>Char at the given index in the Range.</summary>
    /// <param name="index">int offset from the start of the Range.</param>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
    /// <exception cref="ArgumentOutOfRangeException">The index is negative or greater than or
    /// equal to the length of the Range.</exception>
        public char this[long index]
        {
            get
            {
                ValidateThis();
                if (index<0)
                    throw new ArgumentOutOfRangeException("index", "Index may not be negative.");
                if (index>=Length())
                    throw new ArgumentOutOfRangeException("index",
                                                              "Index must be less than Length.");
                Mark mark = _mark1.OffsetMark(index, false);

            // Look out 1: we have found a block but the data isn't necessarily loaded
                StringBuilder sb = mark._block.GetData();

            // Look out 2: if we are at the end of the block we have to go to the next one; we are
            // relying on no empty blocks unless the Text is empty
                if (mark._position==sb.Length) return mark._block._next.GetData()[0];
                else return sb[mark._position];
            }

            set
            {
                ValidateThis();
                if (index<0)
                    throw new ArgumentOutOfRangeException("index", "Index may not be negative.");
                if (index>=Length())
                    throw new ArgumentOutOfRangeException("index",
                                                              "Index must be less than Length.");
                Mark mark = _mark1.OffsetMark(index, false);

            // Look out 1: we have found a block but the data isn't necessarily loaded
                StringBuilder sb = mark._block.GetData();

            // Look out 2: if we are at the end of the block we have to go to the next one; we are
            // relying on no empty blocks unless the Text is empty; mark block changed
                if (mark._position==sb.Length)
                {
                    mark._block._next.GetData()[0] = value;
                    mark._block._next._offset = -1;
                }
                else
                {
                    sb[mark._position] = value;
                    mark._block._offset = -1;
                }

            // See if we have made any Boxes dirty - assume not many, all short
                foreach (Box box in _mark1._text._boxes)
                {
                    if (!box.DirtyRange && mark.Within(box.Range)) box.OnRangeDirty();
                 }
            }
        }

    //---------------------------------------------------------------------------------------------

    /// <summary>Returns true if this Range contains the passed Mark at the start or in the middle
    /// but not at the end position.</summary>
    /// <param name="mark">Mark which may lie within the Range.</param>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
    /// <exception cref="ArgumentException">The passed Mark has been invalidated or is in a 
    /// different Text.</exception>
        public bool Contains(Mark mark)
        {
            ValidateThis();
            mark.Validate(_mark1._text, "mark");
            return mark.DoWithin(this);
        }

    /// <summary><para>Position at the start of the Range.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public Mark Start
        {
            get
            {
                ValidateThis();
                return new Mark(_mark1, true);
            }
        }

    /// <summary><para>Position at the end of the Range.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
    /// <exception cref="InvalidOperationException">This Range has been invalidated.</exception>
        public Mark End
        {
            get
            {
                ValidateThis();
                return new Mark(_mark2, true);
            }

        }
 
    /// <summary>Text which the Range is in.</summary>
        public Text Text
        {
            get { return _mark1._text; }
        }

    /// <summary>Override for default GetHashCode() - returns an index which is unique for Ranges
    /// created with Text.AddRange(). An unregistered Range returns a value of zero, or -1 if the
    /// Range has been invalidated by a change to the text.</summary>
        public override int GetHashCode()
        {
            if (_mark1._ID < 0 || _mark2._ID < 0) _ID = -1;
            return _ID;
        }
    }

}